/*
 *                 Sun Public License Notice
 * 
 * The contents of this file are subject to the Sun Public License
 * Version 1.0 (the "License"). You may not use this file except in
 * compliance with the License. A copy of the License is available at
 * http://www.sun.com/
 * 
 * The Original Code is NetBeans. The Initial Developer of the Original
 * Code is Sun Microsystems, Inc. Portions Copyright 1997-2004 Sun
 * Microsystems, Inc. All Rights Reserved.
 */

package org.netbeans.swing.tabcontrol.plaf;

import org.netbeans.swing.tabcontrol.TabData;
import org.netbeans.swing.tabcontrol.TabbedContainer;
import org.netbeans.swing.tabcontrol.TabDisplayer;

import org.openide.awt.HtmlRenderer;

import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import java.lang.ref.Reference;
import java.lang.ref.SoftReference;
import java.lang.reflect.Field;
import java.util.EventObject;
import java.util.List;

/**
 * A panel that displays a drop down list of items, in several columns if
 * needed, associated with a JTabbedPane
 *
 * @author Tim Boudreau
 */

final class TabListPopup extends JTable implements MouseMotionListener,
        MouseListener {
    /** Reference to the focus owner when addNotify was called.  This is the
     * component that received the mouse event, so it's what we need to listen
     * on to update the selected cell as the user drags the mouse */
    private Component invokingComponent = null;
    /** Cached preferred size value */
    private Dimension prefSize = null;
    /** Flag indicating that the fixed row height has not yet been calculated - 
     * this is for fontsize support */
    boolean needCalcRowHeight = true;

    /** Reference to container for which we display quicklist */
    private TabDisplayer displayer = null;

    /** Reference to the popup object currently showing the default instance,
     * if it is visible */
    private static Popup currentPopup=null;
    /** AWTEventListener which is attached when the popup is shown to ensure
     * that it is closed when it should be.  It is removed after the popup
     * has been hidden. */
    private static AWTEventListener blistener = null;
    /** Reference to the default shared instance */
    private static Reference instance=null;
    /** Time of invocation, used to determine if a mouse release is
     * delayed long enough from a mouse press that it should close
     * the popup, instead of assuming the user wants move-and-click
     * behavior instead of drag-and-click behavior */
    long invocationTime = -1;

    private static final Border rendererBorder = 
        BorderFactory.createEmptyBorder (2, 3, 0, 3);

    private static HtmlRenderer.Renderer renderer = null;

    /** Creates a new instance of TabListPanel */
    private TabListPopup() {
        super (new TabListPopupTableModel());
        //Set up a line border around the edges
        setBorder (
            BorderFactory.createLineBorder(getForeground()));        
        setShowHorizontalLines(false);
        setBackground (UIManager.getColor("ComboBox.background")); //NOI18N
        if (renderer == null) {
            renderer = HtmlRenderer.createRenderer();
        }
        setDefaultRenderer(Object.class, renderer);
    }

    /**
     * Maps tab selected in quicklist to tab index in displayer to select
     * correct tab
     */
    private void setSelectedTab(int row, int col) {
        //Find corresponding index in displayer
        Object o = getTTModel().getValueAt(row, col);
        if (o instanceof TabData) {
            TabData td = (TabData) o;
            List l = displayer.getModel().getTabs();
            int ind = -1;
            for (int i = 0; i < l.size(); i++) {
                if (td.equals(l.get(i))) {
                    ind = i;
                    break;
                }
            }
            if (ind != -1) {
                int old = displayer.getSelectionModel().getSelectedIndex();
                displayer.getSelectionModel().setSelectedIndex(ind);
                //#40665 fix start
                if (displayer.getType() == TabbedContainer.TYPE_EDITOR && ind >= 0 && ind
                        == old) {
                    displayer.getUI().makeTabVisible(ind);
                }
                //#40665 fix end
            }
        }
    }

    public void updateUI() {
        needCalcRowHeight = true;
        super.updateUI();
    }

    public void setFont(Font f) {
        needCalcRowHeight = true;
        super.setFont(f);
    }

    public Component prepareRenderer(TableCellRenderer renderer, int row, int column) {
        //Find our TabData object
        Object value = getTTModel().getValueAt(row, column);
        
        //Set up font, selection, icon, colors, borders
        
        //Under very peculiar circumstances, displayer can be null - this happens on the
        //mac if the popup has been displayed, and then focus is shifted to another
        //application before it has had a chance to paint.  You need swapping or a big
        //hefty garbage collection after the popup is displayed but before it paints 
        //to make it happen
        int selIdx = displayer != null ? displayer.getSelectionModel().getSelectedIndex() : -1;
        boolean isSelTab = selIdx != -1 ? 
            value == displayer.getModel().getTab(selIdx) 
            : false;

        boolean isMouseOver = row == getSelectedRow() && 
            column == getSelectedColumn() && value != null;
            
        JComponent result = /*(JComponent)
            super.prepareRenderer (renderer, row, column); */
            (JComponent)
            renderer.getTableCellRendererComponent(this, value, 
                isMouseOver, isMouseOver, row, column);

        if (value == null) {
            //it's a filler space, we're done
            result.setOpaque(false);
            return result;
        }
        
        if (isSelTab) {
            result.setFont (getFont().deriveFont (Font.BOLD));
        }

        Icon icon = ((TabData) value).getIcon();

        HtmlRenderer.Renderer ren = (HtmlRenderer.Renderer) result;
        ren.setIcon(icon);
        
        if (icon.getIconWidth() > 0) {
            //Max annotated icon width is 24, so to have all the text and all
            //the icons come out aligned, set the icon text gap to the difference
            //plus a two pixel margin
            ren.setIconTextGap (26 - icon.getIconWidth());
        } else {
            //If the icon width is 0, fill the space and add in
            //the extra two pixels so the node names are aligned (btw, this
            //does seem to waste a frightful amount of horizontal space in
            //a tree that can use all it can get)
            ren.setIndent (26);
        }
        
        //The table may not really have focus, but it should always use the focus
        //color for the selection, not controlShadow
        ((HtmlRenderer.Renderer) result).setParentFocused(true);
        result.setBorder (rendererBorder);
        result.setOpaque(true);
        if (isMouseOver) {
            result.setBackground (getSelectionBackground());
            result.setForeground (getSelectionForeground());
        } else {
            result.setBackground (getBackground());
            result.setForeground (getForeground());
        }
 
        return result;
    }


    /**
     * Calculate the height of rows based on the current font.  This is done
     * when the first paint occurs, to ensure that a valid Graphics object is
     * available.
     *
     * @since 1.25
     */
    private void calcRowHeight(Graphics g) {
        Font f = getFont();
        FontMetrics fm = g.getFontMetrics(f);
        //As icons are displayed use maximum from font and icon height
        int rowHeight = Math.max(fm.getHeight(), 16) + 4;
        needCalcRowHeight = false;
        setRowHeight(rowHeight);
    }

    public void attach(TabDisplayer cont) {
        prefSize = null;
        displayer = cont;
        //Calc row height here so that TableModel can adjust number of columns.
        calcRowHeight(getOffscreenGraphics());
        getTTModel().setRowHeight(getRowHeight());
        getTTModel().attach(cont);
        synchronizeColumns(getTTModel().getColumnCount());
        getSelectionModel().clearSelection();
        getSelectionModel().setAnchorSelectionIndex(-1);
        getSelectionModel().setLeadSelectionIndex(-1);
    }

    static SoftReference ctx = null;

    /**
     * Provides an offscreen graphics context so that widths based on character
     * size can be calculated correctly before the component is shown
     */
    public static Graphics2D getOffscreenGraphics() {
        BufferedImage result = null;
        //XXX multi-monitors w/ different resolution may have problems;
        //Better to call Toolkit to create a screen graphics
        if (ctx != null) {
            result = (BufferedImage) ctx.get();
        }
        if (result == null) {
            result = new BufferedImage(10, 10, BufferedImage.TYPE_INT_RGB);
            ctx = new SoftReference(result);
        }
        return (Graphics2D) result.getGraphics();
    }

    private void synchronizeColumns(int count) {
        TableColumnModel mdl = getColumnModel();
        int currCt = mdl.getColumnCount();
        if (currCt < count) {
            for (int i = currCt; i < count; i++) {
                mdl.addColumn(new TableColumn(i, 75, 
                    renderer, null));            }
        } else if (currCt > count) {
            for (int i = currCt - 1; i >= count; i--) {
                mdl.removeColumn(mdl.getColumn(i));
            }
        }
    }

    /** Overridden to calculate a preferred size based on the current optimal
     * number of columns, and set up the preferred width for each column based
     * on the maximum width tab name & icon displayed in it */
    public Dimension getPreferredSize() {
        if (prefSize == null) {
            Insets ins = getInsets();
            
            prefSize = new Dimension(ins.left + ins.top, ins.right + ins.bottom);
            int cols = getColumnCount();
            int rows = getRowCount();
            
            //Iterate the columns
            for (int i=0; i < cols; i++) {
                int columnWidth = 0;
                //For each column, iterate the rows
                for (int j=0; j < rows; j++) {
                    TableCellRenderer ren = getCellRenderer(j,i);
                    Component c = prepareRenderer (ren, j, i);
                    //find the widest cell
                    columnWidth = Math.max (c.getPreferredSize().width, 
                        columnWidth);
                }
                //Add in the max width needed for this column to the total
                //width
                prefSize.width += columnWidth;
                //Store it in the column model so it will be displayed with
                //the right width
                getColumnModel().getColumn(i).setPreferredWidth(columnWidth);
            }
            //Rows will be fixed height, so just multiply it out
            prefSize.height += rows * getRowHeight();
        }
        return prefSize;
    }

    private final TabListPopupTableModel getTTModel() {
        return (TabListPopupTableModel) getModel();
    }

    public boolean isAttached() {
        return displayer != null;
    }

    public void detach() {
        displayer = null;
        getTTModel().detach();
    }

    public void addNotify() {
        super.addNotify();
        addMouseListener(this);
        addMouseMotionListener(this);
        //Set initial selection if there is any field in table
        if ((getRowCount() > 0) && (getColumnCount() > 0)) {
            changeSelection(-1, -1, false, false);
        }
        EventObject eo = EventQueue.getCurrentEvent();
        if (eo != null) {
            if (eo.getSource() instanceof Component) {
                invokingComponent = (Component) eo.getSource();
            }
        }

        if (invokingComponent != null) {
            invokingComponent.addMouseListener(this);
            invokingComponent.addMouseMotionListener(this);
        }
        invocationTime = System.currentTimeMillis();
    }

    public void removeNotify() {
        super.removeNotify();
        removeMouseListener(this);
        removeMouseMotionListener(this);
        if (invokingComponent != null) {
            invokingComponent.removeMouseListener(this);
            invokingComponent.removeMouseMotionListener(this);
            invokingComponent = null;
        }
        detach();
    }

    public void paint(Graphics g) {
        if (needCalcRowHeight) {
            calcRowHeight(g);
        }
        super.paint(g);
    }

    int convertIndex(int row, int column) {
        return column * getRowCount() + row;
    }

    public void mouseClicked(MouseEvent e) {
        e.consume();
    }

    public void mousePressed(MouseEvent e) {
        Point p = e.getPoint();
        p = SwingUtilities.convertPoint((Component) e.getSource(), p, this);
        if (contains(p)) {
            //Otherwise the AWT listener will handle hiding the popup
            int row = getSelectedRow();
            int col = getSelectedColumn();
            setSelectedTab(row, col);
            //Hide window
            hideCurrentPopup();
        }
        e.consume();
    }

    public void mouseReleased(MouseEvent e) {
        if (e.getSource() == invokingComponent) {
            long time = System.currentTimeMillis();
            if (time - invocationTime > 500) {
                mousePressed(e);
            }
        }
        e.consume();
    }

    public void mouseEntered(MouseEvent e) {
        e.consume();
    }

    public void mouseExited(MouseEvent e) {
        clearSelection();
        e.consume();
    }

    //MouseMotionListener
    public void mouseDragged(MouseEvent e) {
        mouseMoved(e);
        e.consume();
    }

    public void mouseMoved(MouseEvent e) {
        Point p = e.getPoint();
        //It may have occured on the button that invoked the tabtable
        if (e.getSource() != this) {
            p = SwingUtilities.convertPoint((Component) e.getSource(), p,
                                            this);
        }

        if (this.contains(p)) {
            int row = rowAtPoint(p);
            int col = columnAtPoint(p);
            changeSelection(row, col, false, false);
        } else {
            clearSelection();
        }
        e.consume();
    }

    public static synchronized void invoke(final TabDisplayer c,
                                           int screenX, int screenY) {
        if (currentPopup != null) {
            hideCurrentPopup();
            return;
        }
        if (c.getModel().size() == 0) {
            return;
        }
        //Get our singleton soft-cached instance
        final TabListPopup tt = sharedInstance();
        //Aim it at the tabbed displayer in question
        tt.attach(c);
        //Get a popup object for the right coordinates.  Offset it to the 
        //left so it appears with its upper right corner under the button
        maybeHackPopupForAqua();
        currentPopup = PopupFactory.getSharedInstance().getPopup(c, tt, screenX - tt.getPreferredSize()
                                                                                  .width,
                                                                 screenY);

        //show it
        currentPopup.show();
        //Use an AWT listener to hide it in certain circumstances
        blistener = new BackupListener(tt);
    }

    /**
     * Focus changes are occasionally missed if the editor has focus while the
     * tab table is active.  This listener ensures that any mouse event on it
     * will indeed close the component.
     */
    private static class BackupListener implements AWTEventListener {
        //XXX could consolidate the property change listener above into this
        //and just have one listener class.
        private TabListPopup tt;

        public BackupListener(TabListPopup tt) {
            this.tt = tt;
            Toolkit.getDefaultToolkit().addAWTEventListener(this,
                                                            AWTEvent.MOUSE_EVENT_MASK
                                                            | AWTEvent.KEY_EVENT_MASK);
        }

        private boolean onTabTable(MouseEvent e) {
            Point p = e.getPoint();
            p = SwingUtilities.convertPoint((Component) e.getSource(), p, tt);
            return tt.contains(p);
        }

        public void eventDispatched(AWTEvent event) {
            if (event.getSource() == tt) {
                return;
            }
            if (event instanceof MouseEvent) {
                if (event.getID() == MouseEvent.MOUSE_RELEASED) {
                    long time = System.currentTimeMillis();
                    if (time - tt.invocationTime > 500) {
                        if (!onTabTable((MouseEvent) event)) {
                            //Don't take any chances
                            Toolkit.getDefaultToolkit()
                                    .removeAWTEventListener(this);
                            hideCurrentPopup();
                        }
                    }
                } else if (event.getID() == MouseEvent.MOUSE_PRESSED) {
                    if (!onTabTable((MouseEvent) event)) {
                        //Don't take any chances
                        if (event.getSource() != tt.invokingComponent) {
                            //If it's the invoker, don't do anything - it
                            //will generate another call to invoke(), which will
                            //do the hiding - if we do it here, it will get
                            //shown again when the button processes the event
                            Toolkit.getDefaultToolkit()
                                    .removeAWTEventListener(this);
                            hideCurrentPopup();
                        }
                    }
                }
            } else if (event instanceof KeyEvent) {
                if (event.getID() == KeyEvent.KEY_PRESSED) {
                    Toolkit.getDefaultToolkit().removeAWTEventListener(this);
                    hideCurrentPopup();
                }
            }
        }

    }

    public synchronized static void hideCurrentPopup() {
        if (currentPopup != null) {
            //Issue 41121 - use invokeLater to allow any pending
            //event processing against the popup contents to run
            //before the popup is hidden
            SwingUtilities.invokeLater (new PopupHider(currentPopup));
            currentPopup = null;
        }
        if (blistener != null) {
            Toolkit.getDefaultToolkit().removeAWTEventListener(blistener);
        }
    }
    
    /** Runnable which hides the popup in a subsequent event queue
     * loop.  This is to avoid problems with BasicToolbarUI, which
     * will try to process events on the component after it has been
     * hidden and throw exceptions.
     * @see http://www.netbeans.org/issues/show_bug.cgi?id=41121
     */
    private static class PopupHider implements Runnable {
        private Popup toHide;
        public PopupHider (Popup popup) {
            toHide = popup;
        }
        
        public void run() {
            toHide.hide();
         }
     }     

    private static TabListPopup sharedInstance() {
        TabListPopup result = null;
        if (instance != null) {
            result = (TabListPopup) instance.get();
        }
        if (result == null) {
            result = new TabListPopup();
            instance = new SoftReference(result);
        }
        return result;
    }

    private static void maybeHackPopupForAqua() {
        //Workaround for JDK bug NNN - a note in the 1.4.2 source for 
        //PopupFactory says:
        
             // MacOSX we want to change the default for pupus to be heavyweight!
            // was: private int popupType = LIGHT_WEIGHT_POPUP;

            // reverting out the change because we need better support in AWT
            // for heavyweights!
            // Steve will put this back in when AWT handles it better (after DP6)
            //private int popupType = HEAVY_WEIGHT_POPUP;        
        
        //but the fix for Apple is actually still commented out, so lightweight
        //popups do not work correctly.  We correct this here via reflection:
        try {
            String osName = System.getProperty ("os.name");
            if ("Mac OS X".equals (osName) || osName.startsWith ("Darwin")) {
                Field toSet = PopupFactory.class.getDeclaredField("popupType");
                toSet.setAccessible(true);
                toSet.set(PopupFactory.getSharedInstance(), new Integer(2));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
